// Copyright 2014 The Go Circuit Project
// Use of this source code is governed by the license for
// The Go Circuit Project, found in the LICENSE file.
//
// Authors:
//   2014 Chris Monson <chris@gocircuit.org>

package ripple

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"math/rand"
	"reflect"
	"sync"
	"time"

	"circuit/kit/rh"
)

// chanRecvFID
type chanRecvFID struct {
	rh.AnchoredFID
	sync.Mutex

	hooks  func() <-chan (chan []byte)
	report func(int64)
	q      rh.Q
	gtm    time.Time

	nonblocking bool
	source      chan []byte
	data        *bytes.Buffer
	bytesRead   int64
}

// NewChanRecv creates a new channel recv file.
// On open, an attempt is made to receive a data channel from hooks.
// When successful, that channel can be used to receive data.
// When nb is specified, the operation is non-blocking, and TryRecv is used
// instead of Recv on the hooks.
func NewChanRecv(hooks func() <-chan chan []byte, report func(int64), nb bool) rh.FID {
	fid := &chanRecvFID{
		hooks:       hooks,
		report:      report,
		nonblocking: nb,
		gtm:         time.Now(),
		q: rh.Q{
			ID:  uint64(rand.Int63()),
			Ver: 1,
		},
	}
	return fid
}

func (fid *chanRecvFID) String() string {
	return fmt.Sprintf("chanrecv (%p) %v", fid, fid.Q())
}

func (fid *chanRecvFID) Q() rh.Q {
	return fid.q
}

func (fid *chanRecvFID) Open(flag rh.Flag, intr rh.Intr) (err error) {
	defer un(lock(fid))

	// avoid blocking simulated opens from the fuse/rh setattr hack
	if flag.Create || flag.Truncate || flag.Deny {
		return rh.ErrPerm
	}

	hooks := fid.hooks()

	// When nonblocking, we use reflect and call TryRecv instead of just using
	// standard receiving techniques.
	if fid.nonblocking {
		hooksValue := reflect.ValueOf(hooks)
		srcValue, _ := hooksValue.TryRecv() // even works if hooks is nil
		if !srcValue.IsValid() {
			// Zero value means that the receive would have had to block.
			return rh.ErrBusy
		}
		fid.source = srcValue.Interface().(chan []byte)
	} else {
		// Block waiting for a hook channel (always has a single writer on the other end).
		select {
		case fid.source = <-hooks:
			// Note that this blocks forever if the channel capacity has not
			// been set. That is fine, because the semantic is to block forever
			// on a nil channel.
		case i := <-intr:
			log.Printf("chanrecv open interrupted: %v", i)
			return rh.ErrGone
		}
	}

	return nil
}

func (fid *chanRecvFID) Read(_ int64, count int, intr rh.Intr) (chunk rh.Chunk, err error) {
	defer un(lock(fid))
	if fid.data == nil {
		select {
		case i := <-intr:
			log.Printf("chanrecv read interrupted: %v", i)
			return nil, rh.ErrGone
		case data, ok := <-fid.source:
			if !ok {
				// Channel closed, nothing to see here.
				// TODO(chris): non-nil error (e.g., rh.ErrEOF) causes weird fuse problems.
				return nil, nil
			}
			fid.bytesRead += int64(len(data))
			fid.data = bytes.NewBuffer(data)
		}
	}
	// Now the internal data buffer is full, so just fulfill the request.
	buff := make([]byte, count)
	n, err := fid.data.Read(buff)
	if err == io.EOF {
		fid.report(fid.bytesRead)
		err = nil // Fuse does not grok EOF.
	}
	return rh.ByteChunk(buff[:n]), err
}

func (fid *chanRecvFID) Write(offset int64, data rh.Chunk, intr rh.Intr) (n int, err error) {
	return 0, rh.ErrPerm
}

func (fid *chanRecvFID) Clunk() {
	// Consume from the source if there is something there, otherwise just bail out.
	select {
	case _, ok := <-fid.source:
		if ok {
			// If the channel was not closed, then we aborted in the middle and
			// nothing has been reported, yet.
			fid.report(fid.bytesRead)
		}
	default:
	}
}

func (fid *chanRecvFID) Walk(wname []string) (rh.FID, error) {
	if len(wname) > 0 {
		return nil, rh.ErrClash
	}
	// Duplicate these guys so that we can have multiple openers.
	return NewChanRecv(fid.hooks, fid.report, fid.nonblocking), nil
}

func (fid *chanRecvFID) Stat() (*rh.Dir, error) {
	return &rh.Dir{
		Q:      fid.q,
		Mode:   rh.Mode{Attr: rh.ModeIO},
		Perm:   0444, // r--r--r--, acts as /dev/null for writing and empty file for reading
		Atime:  fid.gtm,
		Mtime:  fid.gtm,
		Name:   "chanrecv",
		Length: 0,
		Uid:    uid(),
		Gid:    gid(),
	}, nil
}
